#
#= Exifライブラリ
# Morita Keisuke (morita@asobitari.nu)
# 2003,3,10
# Yusuke Hattori (htty@hasumi.info)
# 2007,9,21
#
# このプログラムRuby'sライセンスとします。
# http://www.ruby-lang.org/ja/LICENSE.txt
#

# ファイル先頭のJpegを示すFFD8，APP1マーカを示すFFE1，APP1サイズ2バイト(ビッグエンディアン)，
# Exif識別コード"Exif"+00の計12バイト
EXIF_HEADER = 12

# エンディアンを示す"MM"or"II"のTIFFヘッダ，TIFF識別コード002A，
# 0thIFDへのポインタ00000008の8バイト
TIFF_HEADER = 8

# 上記２つのヘッダサイズの合計
HEADER = EXIF_HEADER + TIFF_HEADER

# １つ１つのタグのサイズ
DIR_ENTRY_SIZE = 12
# 通常HEADER以降は0thIFDで，先頭2バイトがタグの数，以下１つ12バイトのタグが並ぶ．
# そして，タグが終わると2バイト分の次のIFDへのポインタがあり，以降は
# IFDタグに入りきらなかった情報が入っている．ここまでを図で示す．()の中はバイト数．
#
# |EXIF_HEADER(12)|TIFF_HEADER(8)|0thIFDのタグ数(2)|タグ(12)|タグ(12)|...
#
# |次のIFDへのポインタ(2)|タグの値(?)|タグの値(?)|...|次のIFD|...|
#
# １つ１つのIFDタグは，2バイトで表されるタグ番号，2バイトで表されるタグの値のデータ形式，
# 4バイトで表されるタグの値の個数，4バイトで表されるタグの値で構成される．
# タグの値が4バイトを超えるときは，タグの値のところにTIFFヘッダすなわちTIFF_HEADERから
# タグの値の入っている領域の先頭番地へのオフセットが入っている．

module IFD_tag
  # TIFFで利用されるタグ番号．必要なものがあれば適宜追加．
  # http://www2.airnet.ne.jp/~kenshi/exif.html#TAGS
  # などを参照．
  IFD_TAG = Hash.new
  IFD_TAG[0x0100] = "ImageWidth"
  IFD_TAG[0x0101] = "ImageLength"
  IFD_TAG[0x0102] = "BitsPerSample"
  IFD_TAG[0x0103] = "Compression"
  IFD_TAG[0x0106] = "PhotometricInterpretation"
  IFD_TAG[0x010e] = "ImageDescription"
  IFD_TAG[0x010f] = "Maker"
  IFD_TAG[0x0110] = "Model"
  IFD_TAG[0x0111] = "StripOffsets"
  IFD_TAG[0x0112] = "Orientation"
  IFD_TAG[0x0115] = "SamplePerPixel"
  IFD_TAG[0x0116] = "RowsPerStrip"
  IFD_TAG[0x0117] = "StripByteCounts"
  IFD_TAG[0x011a] = "XResolution"
  IFD_TAG[0x011b] = "YResolution"
  IFD_TAG[0x011c] = "PlanarConfiguration"
  IFD_TAG[0x0128] = "ResolutionUnit"
  IFD_TAG[0x0131] = "Software"
  IFD_TAG[0x0132] = "DateTime"
  IFD_TAG[0x013e] = "WhitePoint"
  IFD_TAG[0x013f] = "PrimaryChromaticities"
  IFD_TAG[0x0201] = "JpegInterchangeFormat"
  IFD_TAG[0x0202] = "JpegInterchangeFormatLength"
  IFD_TAG[0x0211] = "YCbCrCoefficients"
  IFD_TAG[0x0213] = "YCbCrPositioning"
  IFD_TAG[0x0214] = "ReferenceBlackWhite"
  IFD_TAG[0x8298] = "Copyright"
  IFD_TAG[0x8769] = "ExifIFDPointer"
  IFD_TAG[0x8825] = "GPSInfoIFDPointer"
  IFD_TAG[0x9003] = "DateTimeOriginal"

  # Exif IFDで用いられるタグ番号
  EIFD_TAG = Hash.new
  EIFD_TAG[36867] = "DateTimeOriginal"

  # GPS IFDで用いられるタグ番号
  GPS_TAG = Hash.new
  GPS_TAG[0x0000] = "GPSVersionID"
  GPS_TAG[0x0001] = "GPSLatitudeRef"
  GPS_TAG[0x0002] = "GPSLatitude"
  GPS_TAG[0x0003] = "GPSLongitudeRef"
  GPS_TAG[0x0004] = "GPSLongitude"
  GPS_TAG[0x0007] = "GPSTimeStamp"

  # タグの値の形式のバイト数の配列
  # [0:未使用,1:BYTE, 2:ASCII, 3:SHORT, 4:LONG, 5:RATIONAL, 6:?,
  #  7:UNDEFINED, 8:?, 9:SLONG, 10:SRATIONAL, 11:?, 12:?]
  TYPE = [-1,1,1,2,4,8,1,1,2,4,8,4,8]
end

class ExifInfo < Hash
  include IFD_tag 
  
  def initialize(buf,type,offset=TIFF_HEADER)
    @type = type
    0.upto(dir_entry_count(buf.slice(0,2))-1){|i|
      a = get_tag_value(buf,i,0)
      
      if IFD_TAG.has_key?(a[0])
        if a[2]*TYPE[a[1]] <= 4 # タグの値のところにはタグの値が入っている
          self[IFD_TAG[a[0]]] = a[3]
        else                    # タグの値のところにはオフセットが入っている
          if (a[1]) == 5 or (a[1]) == 8 #8は何だろう？以下表示は適当に．．．
            self[IFD_TAG[a[0]]] = "#{lpack(buf.slice(a[3]-offset,4))}/#{lpack(buf.slice(a[3]-offset+4,4))}"  
          else
            self[IFD_TAG[a[0]]] = "#{buf.slice(a[3]-offset,TYPE[a[1]]*a[2])}"
          end
        end
      end
    }

    #ここ以降Exif IFDタグの解析
    if self.has_key?("ExifIFDPointer")

      # Exif IFDタグの先頭番地のオフセットを取得
      exif_offset = (self["ExifIFDPointer"] - offset).to_i

      # 以降，下駄を履かせて上と同じ計算をする
      0.upto(dir_entry_count(buf.slice(exif_offset,2))-1){|i|
        a = get_tag_value(buf,i,exif_offset)
        
        if EIFD_TAG.has_key?(a[0])
          if a[2]*TYPE[a[1]] <= 4
            self[EIFD_TAG[a[0]]] = a[3]
          else                 # タグの値のところにはオフセットが入っている
            if (a[1]) == 5 or (a[1]) == 8 #8は何だろう？以下表示は適当に．．．
              self[IFD_TAG[a[0]]] = "#{lpack(buf.slice(a[3]-offset,4))}/#{lpack(buf.slice(a[3]-offset+4,4))}"  
            else
              self[IFD_TAG[a[0]]] = "#{buf.slice(a[3]-offset,TYPE[a[1]]*a[2])}"
            end
          end
        end
      }
    end

    #ここ以降GPS IFDタグの解析
    if self.has_key?("GPSInfoIFDPointer")

      # GPSタグの先頭番地のオフセットを取得
      gps_offset = (self["GPSInfoIFDPointer"] - offset).to_i

      # 以降，下駄を履かせて上と同じ計算をする
      0.upto(dir_entry_count(buf.slice(gps_offset,2))-1){|i|
        a = get_tag_value(buf,i,gps_offset)
        
        if GPS_TAG.has_key?(a[0])
          if a[2]*TYPE[a[1]] <= 4
            self[GPS_TAG[a[0]]] = a[3]
          else
            if (a[1]) == 5 or (a[1]) == 8
              b = []
              a[2].times{|j|
                b << lpack(buf.slice(a[3]-offset+(8*j),4)).to_f / lpack(buf.slice(a[3]-offset+(8*j)+4,4))
              }

              if (GPS_TAG[a[0]] == "GPSLatitude") || (GPS_TAG[a[0]] == "GPSLongitude")
                # 緯度，経度は特別な形式で返す
                self[GPS_TAG[a[0]]] = "#{b[0].to_i}.#{b[1].to_i}.#{sprintf("%.2f",b[2])}"
              else
                tmp = "#{b.shift.to_i}"
                b.each{|x|
                  tmp += " #{x.to_i}"
                }
                self[GPS_TAG[a[0]]] = tmp
              end
            else
              self[GPS_TAG[a[0]]] = "#{buf.slice(a[3]-offset,TYPE[a[1]]*a[2])}"
            end
          end
        end
      }
    end
  end
  # ディレクトリエントリの個数
  def dir_entry_count(buf)
    pack(buf)
  end
  # モトローラ形式ならbufを反転
  def pack(buf)
    h = {"M"=>"n","I"=>"v"}[@type]
    buf.unpack(h)[0]
  end
  def lpack(buf)
    h = {"M"=>"N","I"=>"V"}[@type]
    buf.unpack(h)[0]
  end
end

class ExifThumbnail < Hash
  include IFD_tag
  def initialize(buf,type,offset)
    @buf = buf
    @offset = offset
    @type = type
    0.upto(dir_entry_count(buf.slice(0,2))-1){|i|
      a = get_tag_value(buf,i,0)
      
      if IFD_TAG.has_key?(a[0])
        if a[2]*TYPE[a[1]] <= 4
          self[IFD_TAG[a[0]]] = a[3]
        else
          if (a[1]) == 5 or (a[1]) == 8
            self[IFD_TAG[a[0]]] = "#{lpack(buf.slice(a[3]-offset,4))}/#{lpack(buf.slice(a[3]-offset+4,4))}"  
          else
            self[IFD_TAG[a[0]]] = "#{buf.slice(a[3]-offset+12,TYPE[a[1]]*a[2])}"
          end
        end
      end
    }   
  end
  # ディレクトリエントリの個数
  def dir_entry_count(buf)
    pack(buf)
  end
  def pack(buf)
    h = {"M"=>"n","I"=>"v"}[@type]
    buf.unpack(h)[0]
  end  
  def lpack(buf)
    h = {"M"=>"N","I"=>"V"}[@type]
    buf.unpack(h)[0]
  end  
  def compression
    if self.has_key?("Compression")
      case self["Compression"]
        when 6 then return "JPEG"
        when 1 then return "TIFF"
        else return "Undefined"
      end
    end
  end
  def image
    if compression == "JPEG"
      @buf.slice(self["JpegInterchangeFormat"]-@offset,self["JpegInterchangeFormatLength"])
    else
      nil
    end
  end
end

class Exif
  # 初期化
  def initialize(file)
    @fname = file
    @info = Hash.new
    @tnd = Hash.new
    File.open(file){|f|
      f.binmode
      # Exifヘッダーを取得
      @buf = f.read(EXIF_HEADER)
      if self.exif?
        @buf += f.read(app1_size-EXIF_HEADER)
        
        # Jpeg画像のヘッダーを取得
        # バイト並び形式("I" or "M")
        @data_type = @buf.slice(EXIF_HEADER,1)
         
        # Jpeg画像の情報を取得 ---------------------------------------
        # Jpegディレクトリエントリ数
        jpg_dir_count = dir_entry_count(@buf.slice(HEADER,2))
        # サムネイルエントリ
        thumb_entry = check(@buf.slice(HEADER+2+jpg_dir_count*DIR_ENTRY_SIZE,4)).unpack("v")[0]
        # Jpeg画像情報オブジェクトを作成
        if (thumb_entry == 0) # サムネイルがないとき
          # APP1全体を渡す
          @info = ExifInfo.new(@buf.slice(HEADER..(self.app1_size+2)),@data_type)
        else                  # サムネイルがあるとき
          # 0thIFDを渡す
          @info = ExifInfo.new(@buf.slice(HEADER..thumb_entry),@data_type)
          # サムネイル画像の情報を取得
          # サムネイルディレクトリエントリ数
          thum_dir_count = dir_entry_count(@buf.slice(thumb_entry+EXIF_HEADER,2))
          @tnd = ExifThumbnail.new(@buf.slice(thumb_entry+EXIF_HEADER..@buf.size),@data_type,thumb_entry)
        end
      end
    }
  end  
  # Exifファイルか？
  def exif?
    if @buf.slice(6,6) == "Exif\000\000" then true end
  end
  # Exif情報サイズ
  def app1_size
    @buf.slice(4,2).unpack("n")[0]
  end
  # ディレクトリエントリの個数
  def dir_entry_count(buf)
    check(buf).unpack("v")[0]
  end
  # モトローラ形式ならbufを反転
  def check(buf)
    if @data_type == "M" then buf.reverse
    else buf end
  end
  def image
    @tnd.image
  end
  #
  attr_reader :data_type,:info,:tnd    
end

def get_tag_value(buf,i,offset)
  #タグの入ったバッファと何番目のタグかと先頭タグへのオフセットを受け取り，
  #タグのそれぞれの値を配列で返す
  a = []
  # タグ番号の取得
  a << pack(buf.slice(offset+2+i*12,2))
  # タグの値のタイプの取得
  a << pack(buf.slice(offset+4+i*12,2))
  # タグの値の個数の取得
  a << lpack(buf.slice(offset+6+i*12,4))
  
  if IFD_tag::TYPE[a[1]] == 2 # タグの値が2バイトのときのタグの値の取得
    a << pack(buf.slice(offset+10+i*12,4))
  else               # タグの値が2バイト以外のときのタグの値の取得
    a << lpack(buf.slice(offset+10+i*12,4))
  end
  return a
end